<?php
// $Id: qimage.class.php 10 2011-03-19 03:59:28Z tomqin@gmail.com $

/**
 * QImage 类封装了针对图像的操作
 *
 * 开发者不能直接构造该类的实例，而是应该用 QImage::createFromFile()
 * 静态方法创建一个 Image 类的实例。
 *
 * 操作大图片时，请确保 php 能够分配足够的内存。
 *
 * @author tomqin <tomqin@gmail.com>
 * @copyright Copyright (c) 2009-2012 SupeFun.Com Inc.
 * @package Core
 */

abstract class QImage
{
    /**
     * 从指定文件创建 QImageGD 对象
     *
     * 用法：
     * @code php
     * $image = QImage::createFromFile('1.jpg');
     * $image->resize($width, $height);
     * $image->saveAsJpeg('2.jpg');
     * @endcode
     *
     * 对于上传的文件，由于其临时文件名中并没有包含扩展名。
     * 因此需要采用下面的方法创建 QImage 对象：
     *
     * @code php
     * $ext = pathinfo($_FILES['postfile']['name'], PATHINFO_EXTENSION);
     * $image = QImage::createFromFile($_FILES['postfile']['tmp_name'], $ext);
     * @endcode
     *
     * @param string $filename 图像文件的完整路径
     * @param string $fileext 指定扩展名
     *
     * @return QImageGD 从文件创建的 QImageGD 对象
     * @throw QException
     */
    static function createFromFile($filename, $fileext) {
        $fileext = trim(strtolower($fileext), '.');
        $ext2functions = array(
            'jpg'  => 'imagecreatefromjpeg',
            'jpeg' => 'imagecreatefromjpeg',
            'png'  => 'imagecreatefrompng',
            'gif'  => 'imagecreatefromgif'
        );

        if (!isset($ext2functions[$fileext])) {
        	throw new QException('imagecreateform' . $fileext);
        }

        $handle = call_user_func($ext2functions[$fileext], $filename);
        return new QImageGD($handle);
    }

	/**
	 * 将 16 进制颜色值转换为 rgb 值
     *
     * 用法：
     * <code>
     * <?php
     * $color = '#369';
     * list($r, $g, $b) = QImage::hex2rgb($color);
     * echo "red: {$r}, green: {$g}, blue: {$b}";
     * ?>
     * </code>
     *
     * @param string $color 颜色值
     * @param string $default 使用无效颜色值时返回的默认颜色
	 *
	 * @return array 由 RGB 三色组成的数组
	 */
	static function hex2rgb($color, $default = 'ffffff') {
        $hex = trim($color, '#&Hh');
        $len = strlen($hex);
        if ($len == 3)
        {
            $hex = "{$hex[0]}{$hex[0]}{$hex[1]}{$hex[1]}{$hex[2]}{$hex[2]}";
        }
        elseif ($len < 6)
        {
            $hex = $default;
        }
        $dec = hexdec($hex);
        return array(($dec >> 16) & 0xff, ($dec >> 8) & 0xff, $dec & 0xff);
	}
}

/**
 * QImageGD 类封装了一个 gd 句柄，用于对图像进行操作
 *
 * @author tomqin <tomqin@gmail.com>
 * @copyright Copyright (c) 2009-2012 SupeFun.Com Inc.
 * @package Core
 */
class QImageGD
{
    /**
     * GD 资源句柄
     *
     * @var resource
     */
    protected $_handle = null;

    /**
     * 构造函数
     *
     * @param resource $handle GD 资源句柄
     */
    function __construct($handle) {
        $this->_handle = $handle;
    }

    /**
     * 析构函数
     */
    function __destruct() {
    	$this->destroy();
    }

    /**
     * 快速缩放图像到指定大小（质量较差）
     *
     * @param int $width 新的宽度
     * @param int $height 新的高度
     *
     * @return QImageGD 返回 QImageGD 对象本身，实现连贯接口
     */
    function resize($width, $height) {
        if (is_null($this->_handle)) {
        	return $this;
        }

        $dest = imagecreatetruecolor($width, $height);
        imagecopyresized($dest, $this->_handle, 0, 0, 0, 0, $width, $height, imagesx($this->_handle), imagesy($this->_handle));
        imagedestroy($this->_handle);
        $this->_handle = $dest;
        return $this;
    }

    /**
     * 缩放图像到指定大小（质量较好，速度比 resize() 慢）
     *
     * @param int $width 新的宽度
     * @param int $height 新的高度
     *
     * @return QImageGD 返回 QImageGD 对象本身，实现连贯接口
     */
    function resampled($width, $height) {
        if (is_null($this->_handle)) {
        	return $this;
        }
        $dest = imagecreatetruecolor($width, $height);
        imagecopyresampled($dest, $this->_handle, 0, 0, 0, 0, $width, $height, imagesx($this->_handle), imagesy($this->_handle));
        imagedestroy($this->_handle);
        $this->_handle = $dest;
        return $this;
    }

    /**
     * 调整图像大小，但不进行缩放操作
     *
     * 用法：
     * <code>
     * $image->resizeCanvas($width, $height, 'top-left');
     * </code>
     *
     * $pos 参数指定了调整图像大小时，图像内容按照什么位置对齐。
     * $pos 参数的可用值有：
     *
     * -   left: 左对齐
     * -   right: 右对齐
     * -   center: 中心对齐
     * -   top: 顶部对齐
     * -   bottom: 底部对齐
     * -   top-left, left-top: 左上角对齐
     * -   top-right, right-top: 右上角对齐
     * -   bottom-left, left-bottom: 左下角对齐
     * -   bottom-right, right-bottom: 右下角对齐
     *
     * 如果指定了无效的 $pos 参数，则等同于指定 center。
     *
     * @param int $width 新的高度
     * @param int $height 新的宽度
     * @param string $pos 调整时图像位置的变化
     * @param string $bgcolor 空白部分的默认颜色
     *
     * @return QImageGD 返回 QImageGD 对象本身，实现连贯接口
     */
    function resizeCanvas($width, $height, $pos = 'center', $bgcolor = '0xffffff') {
        if (is_null($this->_handle)) {
        	return $this;
        }

        $dest = imagecreatetruecolor($width, $height);
        $sx = imagesx($this->_handle);
        $sy = imagesy($this->_handle);

        // 根据 pos 属性来决定如何定位原始图片
        switch (strtolower($pos)) {
	        case 'left':
	            $ox = 0;
	            $oy = ($height - $sy) / 2;
	        break;
	        case 'right':
	            $ox = $width - $sx;
	            $oy = ($height - $sy) / 2;
	        break;
	        case 'top':
	            $ox = ($width - $sx) / 2;
	            $oy = 0;
	        break;
	        case 'bottom':
	            $ox = ($width - $sx) / 2;
	            $oy = $height - $sy;
	        break;
	        case 'top-left':
	        case 'left-top':
	            $ox = $oy = 0;
	        break;
	        case 'top-right':
	        case 'right-top':
	            $ox = $width - $sx;
	            $oy = 0;
	        break;
	        case 'bottom-left':
	        case 'left-bottom':
	            $ox = 0;
	            $oy = $height - $sy;
	        break;
	        case 'bottom-right':
	        case 'right-bottom':
	            $ox = $width - $sx;
	            $oy = $height - $sy;
	        break;
	        default:
	            $ox = ($width - $sx) / 2;
	            $oy = ($height - $sy) / 2;
        }

        list ($r, $g, $b) = QImage::hex2rgb($bgcolor, '0xffffff');
        $bgcolor = imagecolorallocate($dest, $r, $g, $b);
        imagefilledrectangle($dest, 0, 0, $width, $height, $bgcolor);
        imagecolordeallocate($dest, $bgcolor);

        imagecopy($dest, $this->_handle, $ox, $oy, 0, 0, $sx, $sy);
        imagedestroy($this->_handle);
        $this->_handle = $dest;

        return $this;
    }

    /**
     * 在保持图像长宽比的情况下将图像裁减到指定大小
     *
     * cropCanvas() 在缩放图像时，可以保持图像的长宽比，从而保证图像不会拉高或压扁。
     *
     * cropCanvas() 默认情况下会按照 $width 和 $height 参数计算出最大缩放比例，
     * 保持裁减后的图像能够最大程度的充满图片。
     *
     * 例如源图的大小是 800 x 600，而指定的 $width 和 $height 是 200 和 100。
     * 那么源图会被首先缩小为 200 x 150 尺寸，然后裁减掉多余的 50 像素高度。
     *
     * 用法：
     * <code>
     * $image->cropCanvas($width, $height);
     * </code>
     *
     * 如果希望最终生成图片始终包含完整图像内容，那么应该指定 $options 参数。
     * 该参数可用值有：
     *
     * -   fullimage: 是否保持完整图像
     * -   pos: 缩放时的对齐方式
     * -   bgcolor: 缩放时多余部分的背景色
     * -   enlarge: 是否允许放大
     * -   reduce: 是否允许缩小
     *
     * 其中 $options['pos'] 参数的可用值有：
     *
     * -   left: 左对齐
     * -   right: 右对齐
     * -   center: 中心对齐
     * -   top: 顶部对齐
     * -   bottom: 底部对齐
     * -   top-left, left-top: 左上角对齐
     * -   top-right, right-top: 右上角对齐
     * -   bottom-left, left-bottom: 左下角对齐
     * -   bottom-right, right-bottom: 右下角对齐
     *
     * 如果指定了无效的 $pos 参数，则等同于指定 center。
     *
     * $options 中的每一个选项都可以单独指定，例如在允许裁减的情况下将图像放到新图片的右下角。
     *
     * <code>
     * $image->cropCanvas($width, $height, array('pos' => 'right-bottom'));
     * </code>
     *
     * @param int $width 新的宽度
     * @param int $height 新的高度
     * @param array $options 裁减选项
     *
     * @return QImageGD 返回 QImageGD 对象本身，实现连贯接口
     */
    function cropCanvas($width, $height, $options = array()) {
        if (is_null($this->_handle)) {
        	return $this;
        }

        $default_options = array(
            'fullimage' => false,
            'pos'       => 'center',
            'bgcolor'   => '0xfff',
            'enlarge'   => false,
            'reduce'    => true,
        );
        $options = array_merge($default_options, $options);

        // 创建目标图像
        $dest = imagecreatetruecolor($width, $height);
        // 填充背景色
        list ($r, $g, $b) = QImage::hex2rgb($options['bgcolor'], '0xffffff');
        $bgcolor = imagecolorallocate($dest, $r, $g, $b);
        imagefilledrectangle($dest, 0, 0, $width, $height, $bgcolor);
        imagecolordeallocate($dest, $bgcolor);

        // 根据源图计算长宽比
        $full_w = imagesx($this->_handle);
        $full_h = imagesy($this->_handle);
        $ratio_w = doubleval($width) / doubleval($full_w);
        $ratio_h = doubleval($height) / doubleval($full_h);

        if ($options['fullimage']) {
            // 如果要保持完整图像，则选择最小的比率
            $ratio = $ratio_w < $ratio_h ? $ratio_w : $ratio_h;
        } else {
            // 否则选择最大的比率
            $ratio = $ratio_w > $ratio_h ? $ratio_w : $ratio_h;
        }

        if (!$options['enlarge'] && $ratio > 1) $ratio = 1;
        if (!$options['reduce'] && $ratio < 1) $ratio = 1;

        // 计算目标区域的宽高、位置
        $dst_w = $full_w * $ratio;
        $dst_h = $full_h * $ratio;

        // 根据 pos 属性来决定如何定位
        switch (strtolower($options['pos'])) {
	        case 'left':
	            $dst_x = 0;
	            $dst_y = ($height - $dst_h) / 2;
	        break;
	        case 'right':
	            $dst_x = $width - $dst_w;
	            $dst_y = ($height - $dst_h) / 2;
	        break;
	        case 'top':
	            $dst_x = ($width - $dst_w) / 2;
	            $dst_y = 0;
	        break;
	        case 'bottom':
	            $dst_x = ($width - $dst_w) / 2;
	            $dst_y = $height - $dst_h;
	        break;
	        case 'top-left':
	        case 'left-top':
	            $dst_x = $dst_y = 0;
	        break;
	        case 'top-right':
	        case 'right-top':
	            $dst_x = $width - $dst_w;
	            $dst_y = 0;
	        break;
	        case 'bottom-left':
	        case 'left-bottom':
	            $dst_x = 0;
	            $dst_y = $height - $dst_h;
	        break;
	        case 'bottom-right':
	        case 'right-bottom':
	            $dst_x = $width - $dst_w;
	            $dst_y = $height - $dst_h;
	        break;
	        case 'center':
	        default:
	            $dst_x = ($width - $dst_w) / 2;
	            $dst_y = ($height - $dst_h) / 2;
        }

        imagecopyresampled($dest,  $this->_handle, $dst_x, $dst_y, 0, 0, $dst_w, $dst_h, $full_w, $full_h);
        imagedestroy($this->_handle);
        $this->_handle = $dest;

        return $this;
    }

    /**
     * 保存为 JPEG 文件
     *
     * @param string $filename 保存文件名
     * @param int $quality 品质参数，默认为 80
     *
     * @return QImageGD 返回 QImageGD 对象本身，实现连贯接口
     */
    function saveAsJpeg($filename, $quality = 80) {
        imagejpeg($this->_handle, $filename, $quality);
    }

    /**
     * 保存为 PNG 文件
     *
     * @param string $filename 保存文件名
     *
     * @return QImageGD 返回 QImageGD 对象本身，实现连贯接口
     */
    function saveAsPng($filename) {
        imagepng($this->_handle, $filename);
    }

    /**
     * 保存为 GIF 文件
     *
     * @param string $filename 保存文件名
     *
     * @return QImageGD 返回 QImageGD 对象本身，实现连贯接口
     */
    function saveAsGif($filename) {
        imagegif($this->_handle, $filename);
    }

    /**
     * 销毁内存中的图像
     *
     * @return QImageGD 返回 QImageGD 对象本身，实现连贯接口
     */
    function destroy() {
    	if (!$this->_handle) {
            @imagedestroy($this->_handle);
    	}
        $this->_handle = null;
        return $this;
    }
}

